﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using ProLinq.Projection;
using ProLinq.Toolkit;
using ProLinq.Wcf.Linq.Providers;

namespace ProLinq.Wcf.Linq.Expressions.Visitors
{
	internal class ClientServiceVisitor : ExpressionVisitor
	{
		public ClientServiceVisitor()
		{
		}

		public override Expression Visit(Expression node)
		{
			var sorcesVisitor = new SourcesVisitor();
			var modifiedExpression = sorcesVisitor.Visit(node);

			// We need to evaluate constants and local method calls before sending the expression to server.
			// However we should not touch parameters (obviously), constructor expressions (these are projections) and sources (these are our method calls)
			modifiedExpression = Evaluator.PartialEval(modifiedExpression, e =>
						e.NodeType != ExpressionType.Parameter &&
						e.NodeType != ExpressionType.New &&
						!sorcesVisitor.Sources.Contains(e)
						);

			return modifiedExpression;
		}

		protected override Expression VisitConstant(ConstantExpression node)
		{
			if (node.Type.Is(typeof (Query<>)))
			{
				var serviceProvider = ((IQueryable) node.Value).Provider as ServiceQueryProvider;
				if (serviceProvider != null)
				{
					return serviceProvider.ServiceMethod;
				}

				var queryType = typeof (IQueryable<>).MakeGenericType(node.Type.GenericTypeArguments);
				return Expression.Constant(null, queryType);
			}
			return base.VisitConstant(node);
		}

		#region Nested

		private class SourcesVisitor : ExpressionVisitor
		{
			public List<Expression> Sources { get; private set; }

			public SourcesVisitor()
			{
				Sources = new List<Expression>();
			}

			protected override Expression VisitConstant(ConstantExpression node)
			{
				if (node.Type.Is(typeof(Query<>)))
				{
					var serviceProvider = ((IQueryable)node.Value).Provider as ServiceQueryProvider;
					if (serviceProvider != null)
					{
						Sources.Add(serviceProvider.ServiceMethod);
						return serviceProvider.ServiceMethod;
					}

					var queryType = typeof(IQueryable<>).MakeGenericType(node.Type.GenericTypeArguments);
					var source = Expression.Constant(null, queryType);

					Sources.Add(source);
					return source;
				}
				return base.VisitConstant(node);
			}
		}

		#endregion
	}
}